#!/usr/bin/env python
# -*- coding: UTF-8 -*-
"""
@Project ：prod_ctrl_service 
@File    ：output_lot.py
@IDE     ：PyCharm 
@Author  ：孔令伟
@Date    ：9/9/24 10:14 AM 
"""
import uuid
from copy import deepcopy
from datetime import datetime
from typing import Dict, List, Union

from mg_app_framework import MesCode, get_organization, rabbitmq_publish_one, get_logger

from common.basic_mongo import get_mongo_collection, get_feeding_config_collection, get_code_generation_collection, \
    get_new_batch_code_config_collection
from common.constant import RetrospectType
from common.retrospect_operate.handle import (publish_retrospect_nodes, filter_device_code_workorder_config)
from common.retrospect_operate.material_tools import get_unique_id
from prod_ctrl_service.tool.tool import (check_basic_param, check_repeat_in_or_out, get_time, insert_prod_result_param,
                                         get_ng_name, insert_unqualified_records_backup, send_unqualified_http
                                         )

"""
完工出站（分切）, 在制品完工上报（本工序产出物为批次的工序），MES报工，通过MES逻辑检查正确性及根据BOM进行扣料
"""


async def check_output_lot(params: Dict[str, Union[str, List]]) -> Dict[str, Union[str, Dict]]:
    """
    在制品完工上报（本工序产出物为批次的工序），MES报工，通过MES逻辑检查正确性及根据BOM进行扣料
    :param params:{
                   "call_id":"JQ00386",       # 接口调用方认证ID
                   "trace_id":"8941d16b-0581-4e68-8940-a651afff4282",    # 接口调用ID
                   "upperpc_code": "UP011",   # 上位机编码
                   "user_code":"Y001",   # 员工编码
                   "equip_code":"ZP001",   # 设备编码
                   "process_code":"2050",   # 工序编码
                   "out_lot_list": [    # 产出批次列表
                      {
                         "wip_no": "YHY800001-001-01",   # 产出批次码
                         "ok_qty": "180",               # 合格数量
                         "ng_list":[
                                    {
                                       "ng_code":"E0001",  # NG代码, string, NG代码
                                       "ng_qty":10  # Y, 该NG代码对应的报废数量
                                    },
                                    {
                                       "ng_code":"E0001",
                                       "ng_qty":10
                                    },
                                    {
                                       "ng_code":"E0001",
                                       "ng_qty":10
                                    }]
                         "src_wip_no":["YHY800001-001","YHY800001-002"],   # 投料批次码
                         "src_qty":["50","150"],    # 投料批次数量
                         "param_list": [    # 结果参数列表
                               {
                                  "param_code": "PM001",    # 参数编码
                                  "param_value": "5.54"     # 参数采集值
                               },
                               {
                                  "param_code": "PM002",
                                  "param_value": "0.98"
                               }
                         ]
                      }
                   ]
                }
    :return:{
           "code": "success",   #接口应答状态代码, success：成功; fail：失败, string
           "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
           "info": "OK"   #接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
        }
    """
    error_msg = ""
    # '接口调用方认证ID/接口调用ID/上位机编码/员工编码/设备编码'是每一个接口都有且必填的入参，检查这些参数是否填写
    basic_param_check_result, basic_param_check_message = check_basic_param(params=params)
    if not basic_param_check_result:
        error_msg += basic_param_check_message
    process_code = params.get("process_code")
    equip_code = params.get("equip_code")
    out_lot_list: List[Dict] = params.get("out_lot_list", [])
    if not out_lot_list:
        error_msg += f"必输字段字段‘产出批次列表’(out_lot_list)未填写;"
    for rec in out_lot_list:
        wip_no = rec.get('wip_no', None)
        ok_qty = rec.get('ok_qty', None)
        src_wip_no = rec.get('src_wip_no', None)
        src_qty = rec.get('src_qty', None)
        param_list = rec.get('param_list', None)
        fields = [wip_no is None, not wip_no, ok_qty is None, src_qty is None, src_wip_no is None, not src_wip_no,
                  param_list is None, not param_list]
        if any(fields):
            error_msg += f"请检查‘产出批次列表’(out_lot_list)内的必输字段是否都填写;"
            break
        for param_field in rec["param_list"]:
            if param_field.get("param_code") and param_field.get("param_value"):
                pass
            else:
                error_msg += f"请检查‘结果参数列表’(param_list)内的必输字段是否都填写;"
                break
        for ng_field in rec["ng_list"]:
            if ng_field.get('ng_qty', None) is None:
                error_msg += f"请检查‘NG数据’(ng_list) 内的必输字段是否都填写;"
                break
    if error_msg:
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,
        }
        return result

    # 校验投入批次码的投料数量是否有剩余
    feeding_num_check_msg = await check_src_wip_no_feeding_num(out_lot_list=out_lot_list)
    if feeding_num_check_msg:
        error_msg += feeding_num_check_msg

    # 校验 条码生成表 有无生成 产出批次码
    wip_no_list = [out_lot_dict.get("wip_no") for out_lot_dict in out_lot_list]
    wip_no_check_msg = await check_code_info(wip_no_list=wip_no_list)
    if wip_no_check_msg:
        error_msg += wip_no_check_msg
    # 校验是否重复出站
    qr_code_has_used = await check_repeat_in_or_out(
        procedure_code=process_code, qr_code_list=wip_no_list, op_type=RetrospectType.PRODUCT_OUT
    )
    if qr_code_has_used:
        error_msg += f"条码{qr_code_has_used}在工序{process_code}上已经有出站记录"
    # todo, marking校验
    # 产出批次码出站追溯数据、投料批次码进站追溯数据
    workorder_config = await filter_device_code_workorder_config(device_code=equip_code)
    work_order = workorder_config.workorder_code
    bom_info = workorder_config.bom_info
    if not bom_info.product_out:
        error_msg += f"设备{equip_code}的BOM信息未配置完整;"
    if error_msg:
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,
        }
        return result

    unique_id = get_unique_id()
    work_order_code = f"{work_order}~{unique_id}"
    retros_documents: dict = {}  # 发送到追溯接口的进出站追溯数据不能有码值重复的，所以此处用dict结构来避免这种情况
    qr_code_material_code_map, mq_message = {}, []  # 码值与其物料信息映射, 待发送至打印机的mq消息
    prod_result_param_list: list = []  # 根据入参构建的需要写入到出站参数表的数据
    unqualified_records: list = []  # 根据入参构建的需要写入到不合格品记录表的数据
    unqualified_records_backup: list = []  # 根据入参构建的需要写入到不合格品记录表的数据的备份
    for out_lot_record in out_lot_list:
        # 构建进站追溯数据
        for _ in out_lot_record["src_wip_no"]:
            if _ not in retros_documents:
                retros_documents.update({
                    _: {"qr_code": _,
                        "op_type": RetrospectType.PRODUCT_IN,
                        "check_result": "",
                        "work_order_code": work_order_code,
                        "ng_list": []}
                })
        # 构建ng-list信息
        ng_list: list = out_lot_record.get("ng_list", [])
        retros_ng_list = []
        total_ng_qty = 0
        for ng_record in ng_list:
            retros_ng_list.append({"code": ng_record["ng_code"], "num": ng_record["ng_qty"]})
            total_ng_qty += ng_record["ng_qty"]
        # 构建出站追溯数据
        if out_lot_record["wip_no"] not in retros_documents:
            retros_documents.update({
                out_lot_record["wip_no"]: {"qr_code": out_lot_record["wip_no"],
                                           "op_type": RetrospectType.PRODUCT_OUT,
                                           "check_result": "",
                                           "work_order_code": work_order_code,
                                           "ng_list": retros_ng_list,
                                           "material_code": bom_info.product_out[0].material_code
                                           }
            })
            # 构建mq消息
            mq_message.append({
                'uid': str(uuid.uuid4()),
                'notify_url': '',
                'variables': {
                    'quality_count': f"{out_lot_record['ok_qty']}M",
                    'scrap_count': f"{total_ng_qty}M",
                    'product_code': bom_info.product_out[0].material_code,
                    'product_name': bom_info.product_out[0].material_name,
                    'product_spec': bom_info.product_out[0].material_spec,
                    'process_name': workorder_config.procedure_name,
                    'batch_num': out_lot_record['wip_no'].split('-')[0],
                    'mark': out_lot_record['wip_no'],
                    'produce_date': get_time()[:16].replace('-', '/'),
                    'user_code': params.get('user_code', '')
                }
            })
            # 构建码值与物料映射关系
            qr_code_material_code_map.update({
                out_lot_record["wip_no"]: {
                    "material_code": bom_info.product_out[0].material_code,
                    "material_name": bom_info.product_out[0].material_name,
                    "ok_qty": round(float(out_lot_record["ok_qty"]), 4)
                }
            })
        # 构建出站参数表数据
        prod_result_param_list.append({
            "call_id": params.get("call_id"),  # 接口调用方认证ID
            "trace_id": params.get("trace_id"),  # 接口调用ID
            "upperpc_code": params.get("upperpc_code"),  # 上位机编码
            "user_code": params.get("user_code"),  # 员工编码
            "equip_code": params.get("equip_code"),  # 设备编码
            "process_code": process_code,  # 工序编码
            "wip_no": out_lot_record["wip_no"],
            "param_list": out_lot_record["param_list"],
            "time": get_time(),
            "data_source": "033"  # 此字段用于标记由哪个接口构建的数据
        })
        # todo，暂定以ng-list是否存在来判断是否需要构建不合格品数据
        if ng_list:
            # 同一个码值的不合格品记录数目取决于有多少个ng-code
            for ng_item in ng_list:
                unqualified_reason_name = await get_ng_name(ng_item['ng_code'])
                unqualified_data = {
                    "qr_code": out_lot_record["wip_no"],  # 码值
                    "product_code": bom_info.product_out[0].material_code,  # 不合格品物料编码
                    "product_name": bom_info.product_out[0].material_name,  # 不合格品物料名称
                    "process_code": workorder_config.procedure_code,  # 生产工序编码
                    "process_name": workorder_config.procedure_name,  # 生产工序名称
                    "device_code": workorder_config.device_code,  # 对应生产设备编码
                    "device_name": workorder_config.device_name,  # 对应生产设备编码
                    "production_date": get_time()[:10],  # 出库日期，格式为 %Y-%m-%d
                    "unqualified_reason_code": ng_item['ng_code'],  # 不合格原因编码（对应不合格品基础配置中的不良品编码）
                    "unqualified_reason_name": unqualified_reason_name,  # 不合格原因（对应不合格品基础配置中的不良品名称）
                    "ok_qty": round(float(out_lot_record["ok_qty"]), 4),  # 合格数
                    "ng_qty": round(float(ng_item["ng_qty"]), 4),  # 不合格数
                    'user_name': params.get('user_code', '')
                }
                unqualified_records.append(unqualified_data)
                unqualified_data_backup = deepcopy(unqualified_data)
                unqualified_data_backup.update({"source": '033'})
                unqualified_records_backup.append(unqualified_data_backup)
    try:
        await publish_retrospect_nodes(device_code=equip_code, retrospect_items=list(retros_documents.values()))
    except ValueError as e:
        error_msg += str(e)

    if error_msg:
        result = {
            "code": MesCode.fail,  # 接口应答状态代码, success：成功; fail：失败, string
            "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
            "info": error_msg,
        }
        return result
    # 根据入参中的投料批次码和投料批次数量进行批次码投料数量扣减操作
    await calculate_batch_code_num(out_lot_list=out_lot_list)
    # 将出站数据写入到原材料批次码导入模拟表
    await insert_new_input_raw(qr_code_material_code_map)
    # 写入出站参数
    if prod_result_param_list:
        await insert_prod_result_param(params=prod_result_param_list)
    # 将不合格品记录发送至不合格品app
    if unqualified_records:
        await send_unqualified_http(unqualified_records)
    # 本地将不合格品记录备份写入备份表
    if unqualified_records_backup:
        await insert_unqualified_records_backup(unqualified_records_backup)
    # 将本次接口调用信息写入本接口的日志表
    data = {
        "call_id": params.get("call_id"),  # 接口调用方认证ID
        "trace_id": params.get("trace_id"),  # 接口调用ID
        "upperpc_code": params.get("upperpc_code"),  # 上位机编码
        "user_code": params.get("user_code"),  # 员工编码
        "equip_code": params.get("equip_code"),  # 设备编码
        "process_code": process_code,  # 工序编码
        "out_lot_list": params.get("out_lot_list"),  # 产出批次列表
        "time": get_time()
    }
    await insert_data(data=data)
    # IF033出站后将出站数据发送至打印机
    if process_code == "C60-1":
        mq_key = "printer:printC601"
    elif process_code == "C60-2":
        # mq_key = "printer:printC602"
        mq_key = "printer:printC601"
    else:
        mq_key = None
    if mq_key:
        for message in mq_message:
            await rabbitmq_publish_one({'key': mq_key, 'data': message}, time_series_data=True)
        get_logger().info(f"check_output_lot:key:{mq_key}, mq_msg:{mq_message}")
    result = {
        "code": MesCode.success,  # 接口应答状态代码, success：成功; fail：失败, string
        "data": {},  # 接口返回结果描述, json数组, 本接口不需要返回数据，返回null
        "info": "OK",  # 接口返回信息描述, 如果为fail, 则为fail的异常描述信息, string
    }
    return result


async def check_code_info(wip_no_list: List[str]) -> str:
    """
    判断正负极分切集卷码是否在MES中已经生成
    :param wip_no_list:
    :return:
    """
    collection = get_code_generation_collection()
    query_filter = {
        "child_codes": {"$in": wip_no_list}
    }
    all_child_codes: set = set()
    async for rec in collection.find(query_filter):
        child_codes: list = rec.get("child_codes", [])
        all_child_codes.update(set(child_codes))

    has_in_batch_code: set = set(wip_no_list) & all_child_codes
    not_found_batch_code: set = set(wip_no_list) - has_in_batch_code

    if not_found_batch_code:
        return f"下列分切极卷码没有在MES中生成:{not_found_batch_code}"
    else:
        return ""


async def insert_data(data):
    """
    :param data:
    :return:
    """
    collection = get_mongo_collection(
        database_name=f"{get_organization()}_mes",
        collection_name=f"input_wip_instation_record_slitting",
    )
    await collection.insert_one(data)


async def calculate_batch_code_num(out_lot_list: List[Dict]):
    """
    根据入参中的投料批次码和投料批次数量进行批次码投料数量扣减操作
    :param out_lot_list:
    :return:
    """
    collection = get_feeding_config_collection()
    for sub_document in out_lot_list:
        src_wip_no_list: list = sub_document["src_wip_no"]
        src_qty_list = sub_document["src_qty"]
        for index, src_wip_no in enumerate(src_wip_no_list):
            src_qty = float(src_qty_list[index])
            await collection.find_one_and_update(
                {"lot_no": src_wip_no},
                {"$inc": {"qty": -1 * src_qty}}
            )


async def check_src_wip_no_feeding_num(out_lot_list: list):
    """
    校验投入批次码的投料数量是否有剩余
    :param out_lot_list:
    :return:
    """
    feeding_coll = get_feeding_config_collection()
    src_wip_list = []
    for rec in out_lot_list:
        src_wip_no = rec.get("src_wip_no", [])
        src_wip_list += src_wip_no
    query_filter = {
        "lot_no": {"$in": list(set(src_wip_list))},
        "qty": {"$gt": 0}
    }
    # 有投料记录且投料数量大于0的原材料批次码
    has_feeding_config: list = await feeding_coll.distinct("lot_no", query_filter)
    # 没有投料记录或有投料记录但投料数量不足的原材料批次码
    no_feeding_config: list = list(set(src_wip_list) - set(has_feeding_config))

    if no_feeding_config:
        return f"下列投入批次码校验不通过，原因是未投料或投料数量不足:{no_feeding_config}"
    else:
        return ""


async def insert_new_input_raw(qr_code_material_code_map: dict):
    """
    将出站数据写入到原材料批次码导入模拟表
    :param qr_code_material_code_map:
    :return:
    """
    collection = get_new_batch_code_config_collection()
    insert_data_list = []
    now = datetime.now()
    for qr_code, info in qr_code_material_code_map.items():
        record = {
            "material_code": info["material_code"],
            "material_name": info["material_name"],
            "qr_code": qr_code,
            "total_num": info["ok_qty"],
            "available_num": info["ok_qty"],
            "unit": "",
            "production_date": "",
            "delivery_date": "",
            "effective_date": "",
            "batch_num": "",
            "supplier_code": "",
            "supplier_name": "",
            "supplier_mark": "",
            "create_time": now
        }
        insert_data_list.append(record)

    await collection.insert_many(insert_data_list)
